using Beaker.OpenCube.Game;
using Beaker.OpenCube.Mapping;
using Beaker.OpenCube.Nbt;
using Beaker.Services.Interfaces;
using Beaker.Services.Locator;
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;

namespace Beaker.OpenCube
{
	/// <summary>
	/// Class that process data for a level of the game.
	/// </summary>
	public sealed class DynamicWorld
	{
		private readonly IServiceLocator _serviceLocator;
		private readonly IIOService _ioService;
		private readonly DynamicGame _owner;

		/// <summary>
		/// Creates a new level reader for reading game level information.
		/// </summary>
		/// <param name="serviceLocator">The service locator that provides the services, or <c>null</c> to find it by <see cref="ServiceLocator.Current"/>, or auto initalize using a <see cref="PreInitializedServiceLocator"/>.</param>
		/// <param name="owner">The <see cref="DynamicGame"/> used to open this level reader.</param>
		/// <param name="worldFolderPath"></param>
		internal DynamicWorld( IServiceLocator serviceLocator, DynamicGame owner, string worldFolderPath )
		{
			Contract.Requires<ArgumentNullException>( owner != null );
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( worldFolderPath ) );

			_serviceLocator = serviceLocator ?? ServiceLocator.Current ?? PreInitializedServiceLocator.Initialize();
			_ioService = _serviceLocator.GetInstance<IIOService>();

			if( _ioService == null )
				throw new ArgumentException( "The service locator did not provided an implementation for IIOService", "serviceLocator" );

			_owner = owner;
			_worldFolderPath = worldFolderPath;
		}

		[ContractInvariantMethod]
		private void ContractInvariant()
		{
			Contract.Invariant( _owner != null );
			Contract.Invariant( _serviceLocator != null );
			Contract.Invariant( _ioService != null );
			Contract.Invariant( !string.IsNullOrEmpty( _worldFolderPath ) );
		}

		private readonly string _worldFolderPath;

		/// <summary>
		/// Gets the path the the folder of the world.
		/// </summary>
		/// <value>
		/// The world folder path.
		/// </value>
		public string WorldFolderPath
		{
			get
			{
				Contract.Ensures( !string.IsNullOrEmpty( Contract.Result<string>() ) );
				return _worldFolderPath;
			}
		}

		private readonly WeakReference<LevelData> _levelData = new WeakReference<LevelData>( null );

		/// <summary>
		/// Gets the world data.
		/// </summary>
		/// <value>
		/// The world data.
		/// </value>
		public LevelData LevelData
		{
			get
			{
				return _levelData.GetOrCreate( ReadLevelData );
			}
		}

		/// <summary>
		/// Reads the level data.
		/// </summary>
		/// <returns>The level data.</returns>
		private LevelData ReadLevelData()
		{
			// path to level.dat
			string levelDataFilePath = Path.Combine( _worldFolderPath, "level.dat" );

			// open leve.dat
			using( Stream stream = _ioService.FileOpenRead( levelDataFilePath ) )
			{
				// deserialize nbt file
				var dictionary = NbtReader.Deserialize( stream );
				if( dictionary != null )
				{
					// level.dat contains root Data node
					var data = dictionary["Data"] as IDictionary<string, object>;
					if( data != null )
					{
						// create a mapper that maps to LevelData
						var mapper = new Mapper<LevelData>();
						// map and return
						return mapper.MapDictionary( data );
					}
				}
			}

			return null;
		}

		/// <summary>
		/// Gets a list of all the known names of players that are known for this level.
		/// </summary>
		/// <returns>A sequence of player names</returns>
		public IEnumerable<string> GetPlayerNames()
		{
			// player information resides in 'players' folder
			string playersPath = Path.Combine( _worldFolderPath, "players" );
			if( _ioService.FolderExists( playersPath ) )
			{
				// this folder should contain a *.dat file for every player
				var datFilePaths = _ioService.GetFiles( playersPath, "*.dat" );

				foreach( string datFilePath in datFilePaths )
					yield return Path.GetFileNameWithoutExtension( datFilePath );
			}
		}

		/// <summary>
		/// Get the player data for the player with the given name.
		/// </summary>
		/// <param name="playerName">The name of the player to get the player data for.</param>
		/// <returns>
		/// The player data for the player with the given name.
		/// </returns>
		/// <exception cref="System.ArgumentOutOfRangeException">playerName does not resolve to player data.</exception>
		public PlayerData GetPlayerData( string playerName )
		{
			Contract.Requires<ArgumentNullException>( !string.IsNullOrEmpty( playerName ) );
			Contract.Ensures( Contract.Result<PlayerData>() != null );

			// player information resides in 'players' folder
			string playersPath = Path.Combine( _worldFolderPath, "players" );
			if( _ioService.FolderExists( playersPath ) )
			{
				// this folder should contain a *.dat file for requested player
				string datFilePath = Path.Combine( playersPath, playerName + ".dat" );

				if( _ioService.FileExists( datFilePath ) )
				{
					// open player file
					using( Stream stream = _ioService.FileOpenRead( datFilePath ) )
					{
						// deserialize and map
						var dictionary = NbtReader.Deserialize( stream );
						var mapper = Mapper.Create<PlayerData>();
						return mapper.MapDictionary( dictionary["Data"] as IDictionary<string, object> );
					}
				}
			}

			throw new ArgumentOutOfRangeException( "playerName" );
		}


		/// <summary>
		/// Gets the available dimensions in this level.
		/// </summary>
		/// <returns>Sequence of all the available dimensions in this level.</returns>
		public IEnumerable<Dimension> GetDimensions()
		{
			// Rules:
			// * Dimension folder not there, dimension not available
			// * Dimension folder there, check content for generated content, none, 
			//	then dimension not available in current save.

			// overworld always available
			yield return Dimension.Overworld;

			// now scan folder for dim folders
			IEnumerable<string> folders =
				_ioService.GetFolders( _worldFolderPath )
					.Where( f => Path.GetFileName( f ).StartsWith( "DIM" ) );

			foreach( string folder in folders )
			{
				// now check per DIM folder if there is data
				var reader = new DynamicDimension( _serviceLocator, folder );
				if( reader.HasChunks )
				{
					// when chunck data, determine dimension based on name
					int dimValue;
					if( int.TryParse( folder, NumberStyles.Integer, CultureInfo.InvariantCulture, out dimValue ) )
					{
						// we have a int, ignore if value is inside Dimenion Enum, 
						// so we won't error on new version of the game that adds a new dimension
						yield return (Dimension)dimValue;
					}
				}
			}
		}

		/// <summary>
		/// Opens the given dimension.
		/// </summary>
		/// <param name="dimensionToOpen">The dimension to open.</param>
		/// <returns>The dynamic dimension reader for the given dimension.</returns>
		public DynamicDimension OpenDimension( Dimension dimensionToOpen )
		{
			return new DynamicDimension(
				_serviceLocator,
				Path.Combine( _worldFolderPath, dimensionToOpen.ToFolderName() )
			);
		}
	}
}